Repository navigation
feat(admin,trpc): company metrics dashboard from PostHog, Stripe, Neon, and Mercury - #6410
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe PR replaces the configurable admin analytics dashboard with fixed Product and Business metric sections. It adds PostHog insight retrieval, Stripe and Neon business procedures, reusable metric tiles, and removes obsolete dashboard and user-management surfaces. ChangesCompany dashboard
Estimated code review effort: 4 (Complex) | ~60 minutes Mergeability Score: 🟠 High · up to This PR replaces the admin dashboard with live PostHog, Stripe, and Neon metrics and adds polling for Sigma results. At the current head, several unresolved issues can produce incorrect business metrics or cause slow and failed upstream requests to block or repeatedly overload dashboard requests, so the PR should not merge without fixes or explicit owner acceptance. Sequence Diagram(s)sequenceDiagram
participant Admin
participant DashboardPage
participant tRPC
participant PostHog
participant StripeSigma
participant Neon
Admin->>DashboardPage: open Company Metrics
DashboardPage->>tRPC: request insight and business metrics
tRPC->>PostHog: fetch allowlisted insights
tRPC->>StripeSigma: fetch MRR data
tRPC->>Neon: query subscription metrics
PostHog-->>tRPC: return insight results
StripeSigma-->>tRPC: return MRR points or unavailable state
Neon-->>tRPC: return business metric rows
tRPC-->>DashboardPage: return tile data and states
DashboardPage-->>Admin: render Product and Business tiles
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…admin Adds an allowlisted registry of the eight event-based insights on the company metrics dashboard and a thin getInsightResults adminProcedure (refresh=blocking; PostHog's own result cache is the caching layer). Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…urge stale admin Admin's dashboard becomes a one-to-one mirror of the PostHog company metrics dashboard: product tiles reference the eight canonical saved insights by id (PostHog stays the compute engine and cache); business tiles compute live — MRR from Stripe's own Sigma MRR report (scheduled query "admin-mrr", graceful unavailable state until Sigma is enabled), churn cohorts / logo retention / signup→paid conversion from Neon. Purged per plan D-11/D-12: users page (unbounded listUsers findMany), dead auth_completed retention card, WAU/signups/revenue/traffic charts, leaderboard, demo countdown, range pickers, and their sole-consumer procedures; posthog-client shrinks to the insight fetcher. Plan: plans/20260812-1845-admin-company-dashboard.md Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
a017881 to
0d2a0d1
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useNewWorkspaceScreenVariant/useNewWorkspaceScreenVariant.ts (1)
35-56: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winReset
variantbefore each evaluation.Line 36 leaves the prior assignment in state after the surface closes. If flags do not arrive on a later open, the fallback preserves a prior
"test"value because it only replacesnull.DashboardNewWorkspaceModalcan then redirect with a stale assignment.Clear
variantwhen the surface closes and before a non-override evaluation starts.Proposed fix
useLayoutEffect(() => { - if (!isOpen) return; + if (!isOpen) { + setVariant(null); + return; + } if (overrideEnabled) { setVariant("test"); return; } + setVariant(null);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useNewWorkspaceScreenVariant/useNewWorkspaceScreenVariant.ts` around lines 35 - 56, The useLayoutEffect evaluation flow must reset variant before re-evaluating and when the modal closes. Update the effect around evaluate and the isOpen guard to clear variant to null on close and before starting any non-override flag evaluation, while preserving the overrideEnabled test assignment and fallback behavior.
🧹 Nitpick comments (5)
apps/admin/src/app/(dashboard)/components/AppSidebar/AppSidebar.tsx (1)
37-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider removing the now-empty sections scaffold.
sectionsis always empty, so thesections.map(...)block on Lines 78-116 and theCollapsibleimports are dead code. The PR removes stale admin surfaces, so deleting the scaffold keeps this file consistent with that goal. If you plan to add sidebar groups again soon, keep it as is.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/admin/src/app/`(dashboard)/components/AppSidebar/AppSidebar.tsx around lines 37 - 40, Remove the unused empty sections scaffold declared as sections, along with the sections.map rendering block and its associated Collapsible imports in AppSidebar. Preserve the remaining sidebar content and behavior.packages/trpc/src/router/analytics/business.ts (1)
205-231: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the conversion predicate instead of duplicating the EXISTS block.
The same correlated
EXISTSsubquery appears twice, once forconvertedand once forconversion_pct. Any edit must be applied in both places. Computeconvertedin an inner select or a lateral join, then derive the percentage from that column.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/trpc/src/router/analytics/business.ts` around lines 205 - 231, Refactor the cohort query to evaluate the correlated conversion EXISTS predicate once, using an inner select or lateral join to expose the converted count/flag. Make both converted and conversion_pct derive from that shared result, removing the duplicated EXISTS block while preserving the existing statuses and 30-day window.apps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsx (1)
77-77: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winRound the survival percentage before display.
Line 72 rounds the value for the background color, but line 77 renders
value.surviving_pctunrounded. If the server returns an unrounded ratio, the cell shows a long decimal, for example33.33333333333333%. The siblingRetentionGridTilerounds the displayed percentage.♻️ Proposed formatting fix
- {value ? `${value.surviving_pct}%` : "—"} + {value ? `${Math.round(value.surviving_pct)}%` : "—"}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/admin/src/app/`(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsx at line 77, Update the survival percentage rendering in ChurnHeatmapTile to round value.surviving_pct before appending the percent sign, matching the rounding already used for the background color and the sibling RetentionGridTile display.apps/admin/src/app/(dashboard)/page.tsx (1)
43-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument the expected HogQL column order for each tile.
The
xColumnandseries[].columnvalues are positional indices into the saved PostHog HogQL result. A maintainer who edits the saved insight SQL and reorders or inserts aSELECTcolumn silently changes the meaning of these indices. The tile then renders a plausible chart with the wrong series, and no error surfaces.Add a short comment above each
HogQLLineTilethat records the expected column list, for example-- 0: week, 1: new_creators, 2: activated, 3: activation_pct. This gives the next editor a local reference.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/admin/src/app/`(dashboard)/page.tsx around lines 43 - 90, Add a concise comment immediately above each affected HogQLLineTile recording the saved HogQL result’s expected positional columns, including index 0 used by xColumn and every series key used by that tile. Ensure the comments accurately match the current activationRate, activeOrgs, and workspacePercentiles column mappings without changing chart configuration.apps/admin/src/app/(dashboard)/components/PostHogFunnelTile/PostHogFunnelTile.tsx (1)
26-34: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueValidate both funnel step fields before mapping.
Es6Yu3Lris documented as a flat six-step array of step objects, so nested breakdown arrays are not expected. If this tile must tolerate malformed PostHog results, require a stringnameand finite numericcountbefore mapping. Acount-only guard still permits rows with an undefined name.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/admin/src/app/`(dashboard)/components/PostHogFunnelTile/PostHogFunnelTile.tsx around lines 26 - 34, Update the steps validation before the mapping in PostHogFunnelTile to retain only flat objects with a string name (or custom_name, if supported) and a finite numeric count. Do not rely solely on Array.isArray; reject nested breakdown arrays and rows with undefined names before calculating firstCount and mapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/admin/src/app/`(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx:
- Around line 53-70: Update InsightTileFrame to show the error state only when
no tile data is available, so cached children remain visible during failed
background refetches. Add a derived data-presence prop to InsightTileFrame and
pass it from every caller based on that tile’s cached data, while preserving the
existing loading, empty, and children rendering behavior.
In `@packages/trpc/src/router/analytics/business.ts`:
- Around line 118-140: Update the cohort survival query around the subscriptions
CTE and month-offset aggregation so cohort_size is computed once per cohort and
reused as the fixed denominator for every reported offset. Replace the
per-subscription anniversary filter on created_at with a cohort-level cutoff
based on the cohort month boundary, while retaining only offsets whose cohort
period is complete and preserving the existing output fields and ordering.
- Around line 156-184: Extend the months CTE series in the analytics retention
query one month beyond the current newest base month so active contains the
comparison month for the final row. Keep the outer WHERE filter on b.m
unchanged, and preserve the existing self-join and aggregation behavior.
- Around line 40-43: Configure bounded request timeouts for all listed outbound
dashboard calls: in packages/trpc/src/router/analytics/business.ts at lines
40-43, add an AbortSignal.timeout(...) signal to the scheduled-query-runs list
request and also to the result-file request around line 68; in
packages/trpc/src/lib/posthog-client.ts at lines 15-22, add the same timeout
signal to the refresh=blocking insight request. Use the existing fetch options
while ensuring every affected call aborts after a defined timeout.
- Around line 77-94: Replace the manual split logic in the Sigma CSV parsing
flow with the project’s quote-aware CSV parser, ensuring it supports quoted
commas and CRLF line endings. Preserve the existing column validation, point
construction, filtering, and sorting behavior around the parsed header and rows.
---
Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useNewWorkspaceScreenVariant/useNewWorkspaceScreenVariant.ts`:
- Around line 35-56: The useLayoutEffect evaluation flow must reset variant
before re-evaluating and when the modal closes. Update the effect around
evaluate and the isOpen guard to clear variant to null on close and before
starting any non-override flag evaluation, while preserving the overrideEnabled
test assignment and fallback behavior.
---
Nitpick comments:
In `@apps/admin/src/app/`(dashboard)/components/AppSidebar/AppSidebar.tsx:
- Around line 37-40: Remove the unused empty sections scaffold declared as
sections, along with the sections.map rendering block and its associated
Collapsible imports in AppSidebar. Preserve the remaining sidebar content and
behavior.
In
`@apps/admin/src/app/`(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsx:
- Line 77: Update the survival percentage rendering in ChurnHeatmapTile to round
value.surviving_pct before appending the percent sign, matching the rounding
already used for the background color and the sibling RetentionGridTile display.
In
`@apps/admin/src/app/`(dashboard)/components/PostHogFunnelTile/PostHogFunnelTile.tsx:
- Around line 26-34: Update the steps validation before the mapping in
PostHogFunnelTile to retain only flat objects with a string name (or
custom_name, if supported) and a finite numeric count. Do not rely solely on
Array.isArray; reject nested breakdown arrays and rows with undefined names
before calculating firstCount and mapping.
In `@apps/admin/src/app/`(dashboard)/page.tsx:
- Around line 43-90: Add a concise comment immediately above each affected
HogQLLineTile recording the saved HogQL result’s expected positional columns,
including index 0 used by xColumn and every series key used by that tile. Ensure
the comments accurately match the current activationRate, activeOrgs, and
workspacePercentiles column mappings without changing chart configuration.
In `@packages/trpc/src/router/analytics/business.ts`:
- Around line 205-231: Refactor the cohort query to evaluate the correlated
conversion EXISTS predicate once, using an inner select or lateral join to
expose the converted count/flag. Make both converted and conversion_pct derive
from that shared result, removing the duplicated EXISTS block while preserving
the existing statuses and 30-day window.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2c863d9d-1aca-4c5a-8033-d6a88cf7fbb9
📒 Files selected for processing (55)
apps/admin/src/app/(dashboard)/components/AppSidebar/AppSidebar.tsxapps/admin/src/app/(dashboard)/components/ArpuTile/ArpuTile.tsxapps/admin/src/app/(dashboard)/components/ArpuTile/index.tsapps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsxapps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/index.tsapps/admin/src/app/(dashboard)/components/DemoCountdown/DemoCountdown.tsxapps/admin/src/app/(dashboard)/components/DemoCountdown/index.tsapps/admin/src/app/(dashboard)/components/HogQLLineTile/HogQLLineTile.tsxapps/admin/src/app/(dashboard)/components/HogQLLineTile/index.tsapps/admin/src/app/(dashboard)/components/InsightTileFrame/InsightTileFrame.tsxapps/admin/src/app/(dashboard)/components/InsightTileFrame/index.tsapps/admin/src/app/(dashboard)/components/LeaderboardTable/LeaderboardTable.tsxapps/admin/src/app/(dashboard)/components/LeaderboardTable/index.tsapps/admin/src/app/(dashboard)/components/LogoRetentionTile/LogoRetentionTile.tsxapps/admin/src/app/(dashboard)/components/LogoRetentionTile/index.tsapps/admin/src/app/(dashboard)/components/MrrTile/MrrTile.tsxapps/admin/src/app/(dashboard)/components/MrrTile/index.tsapps/admin/src/app/(dashboard)/components/PostHogFunnelTile/PostHogFunnelTile.tsxapps/admin/src/app/(dashboard)/components/PostHogFunnelTile/index.tsapps/admin/src/app/(dashboard)/components/RetentionCard/RetentionCard.tsxapps/admin/src/app/(dashboard)/components/RetentionCard/index.tsapps/admin/src/app/(dashboard)/components/RetentionGridTile/RetentionGridTile.tsxapps/admin/src/app/(dashboard)/components/RetentionGridTile/index.tsapps/admin/src/app/(dashboard)/components/RevenueTrendChart/RevenueTrendChart.tsxapps/admin/src/app/(dashboard)/components/RevenueTrendChart/index.tsapps/admin/src/app/(dashboard)/components/SignupToPaidTile/SignupToPaidTile.tsxapps/admin/src/app/(dashboard)/components/SignupToPaidTile/index.tsapps/admin/src/app/(dashboard)/components/SignupsTrendChart/SignupsTrendChart.tsxapps/admin/src/app/(dashboard)/components/SignupsTrendChart/index.tsapps/admin/src/app/(dashboard)/components/TimeRangePicker/TimeRangePicker.tsxapps/admin/src/app/(dashboard)/components/TimeRangePicker/index.tsapps/admin/src/app/(dashboard)/components/TrafficSourcesChart/TrafficSourcesChart.tsxapps/admin/src/app/(dashboard)/components/TrafficSourcesChart/index.tsapps/admin/src/app/(dashboard)/components/TrendSeriesTile/TrendSeriesTile.tsxapps/admin/src/app/(dashboard)/components/TrendSeriesTile/index.tsapps/admin/src/app/(dashboard)/components/WAUTrendChart/WAUTrendChart.tsxapps/admin/src/app/(dashboard)/components/WAUTrendChart/index.tsapps/admin/src/app/(dashboard)/components/WeekPicker/WeekPicker.tsxapps/admin/src/app/(dashboard)/components/WeekPicker/index.tsapps/admin/src/app/(dashboard)/page.tsxapps/admin/src/app/(dashboard)/users/components/UsersTable/UsersTable.tsxapps/admin/src/app/(dashboard)/users/components/UsersTable/index.tsapps/admin/src/app/(dashboard)/users/page.tsxapps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsxapps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useNewWorkspaceScreenVariant/useNewWorkspaceScreenVariant.tspackages/shared/src/constants.tspackages/trpc/src/env.tspackages/trpc/src/index.tspackages/trpc/src/lib/posthog-client.tspackages/trpc/src/root.tspackages/trpc/src/router/admin/admin.tspackages/trpc/src/router/analytics/analytics.tspackages/trpc/src/router/analytics/business.tspackages/trpc/src/router/analytics/insight-registry.tsplans/done/20260812-1845-admin-company-dashboard.md
💤 Files with no reviewable changes (21)
- apps/admin/src/app/(dashboard)/components/LeaderboardTable/index.ts
- apps/admin/src/app/(dashboard)/components/TrafficSourcesChart/index.ts
- apps/admin/src/app/(dashboard)/users/components/UsersTable/index.ts
- apps/admin/src/app/(dashboard)/components/WAUTrendChart/index.ts
- apps/admin/src/app/(dashboard)/components/SignupsTrendChart/index.ts
- apps/admin/src/app/(dashboard)/components/DemoCountdown/DemoCountdown.tsx
- apps/admin/src/app/(dashboard)/components/TimeRangePicker/index.ts
- apps/admin/src/app/(dashboard)/components/DemoCountdown/index.ts
- apps/admin/src/app/(dashboard)/components/RetentionCard/index.ts
- apps/admin/src/app/(dashboard)/users/components/UsersTable/UsersTable.tsx
- apps/admin/src/app/(dashboard)/users/page.tsx
- apps/admin/src/app/(dashboard)/components/WeekPicker/index.ts
- apps/admin/src/app/(dashboard)/components/LeaderboardTable/LeaderboardTable.tsx
- apps/admin/src/app/(dashboard)/components/TimeRangePicker/TimeRangePicker.tsx
- apps/admin/src/app/(dashboard)/components/WeekPicker/WeekPicker.tsx
- apps/admin/src/app/(dashboard)/components/RevenueTrendChart/index.ts
- apps/admin/src/app/(dashboard)/components/SignupsTrendChart/SignupsTrendChart.tsx
- apps/admin/src/app/(dashboard)/components/TrafficSourcesChart/TrafficSourcesChart.tsx
- apps/admin/src/app/(dashboard)/components/RetentionCard/RetentionCard.tsx
- apps/admin/src/app/(dashboard)/components/WAUTrendChart/WAUTrendChart.tsx
- apps/admin/src/app/(dashboard)/components/RevenueTrendChart/RevenueTrendChart.tsx
| {isLoading ? ( | ||
| <div className="space-y-3"> | ||
| <Skeleton className="h-6 w-full" /> | ||
| <Skeleton className="h-6 w-4/5" /> | ||
| <Skeleton className="h-6 w-3/5" /> | ||
| </div> | ||
| ) : error ? ( | ||
| <div className="flex h-[200px] items-center justify-center"> | ||
| <p className="text-destructive select-text cursor-text text-sm"> | ||
| {error.message} | ||
| </p> | ||
| </div> | ||
| ) : empty ? ( | ||
| <div className="flex h-[200px] items-center justify-center rounded-md border border-dashed"> | ||
| <p className="text-muted-foreground text-sm">{emptyLabel}</p> | ||
| </div> | ||
| ) : ( | ||
| children |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate file map ---'
ast-grep outline 'apps/admin/src/app/(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx' --view expanded
printf '%s\n' '--- frame source ---'
cat -n 'apps/admin/src/app/(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx'
printf '%s\n' '--- frame usages and state props ---'
rg -n -C 4 'InsightTileFrame|isLoading|emptyLabel|error' 'apps/admin/src/app/(dashboard)' -g '*.tsx' -g '*.ts'Repository: superset-sh/superset
Length of output: 41681
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- React Query dependency and query definitions ---'
rg -n -C 3 '"`@tanstack/react-query`"|`@tanstack/react-query`|useQuery\(' apps/admin package.json bun.lockb bun.lock 2>/dev/null || true
printf '%s\n' '--- InsightTileFrame callers ---'
for f in \
'apps/admin/src/app/(dashboard)/components/LogoRetentionTile/LogoRetentionTile.tsx' \
'apps/admin/src/app/(dashboard)/components/TrendSeriesTile/TrendSeriesTile.tsx' \
'apps/admin/src/app/(dashboard)/components/RetentionGridTile/RetentionGridTile.tsx' \
'apps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsx' \
'apps/admin/src/app/(dashboard)/components/HogQLLineTile/HogQLLineTile.tsx' \
'apps/admin/src/app/(dashboard)/components/SignupToPaidTile/SignupToPaidTile.tsx' \
'apps/admin/src/app/(dashboard)/components/MrrTile/MrrTile.tsx'; do
printf '\n--- %s ---\n' "$f"
cat -n "$f"
done
printf '%s\n' '--- relevant query hooks ---'
rg -n -C 5 'logoRetention|funnelTrends|retention|churn|HogQL|signupToPaid|mrr|useTRPC' 'apps/admin/src' -g '*.ts' -g '*.tsx'Repository: superset-sh/superset
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
files = [
Path("apps/admin/src/app/(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx"),
Path("apps/admin/src/app/(dashboard)/components/MetricCard/MetricCard.tsx"),
]
for path in files:
print(f"--- {path} ---")
for i, line in enumerate(path.read_text().splitlines(), 1):
print(f"{i:4}: {line}")
PY
printf '%s\n' '--- query result-state handling ---'
rg -n -C 8 'useQuery|queryKey|queryFn|retry|staleTime|refetch|isFetching|isLoading|error' \
'apps/admin/src/app/(dashboard)' -g '*.ts' -g '*.tsx'Repository: superset-sh/superset
Length of output: 50376
🌐 Web query:
TanStack Query v5 official docs query states data retained after refetch error isLoading isFetching
💡 Result:
In TanStack Query v5, if a background refetch fails while you have previously successfully fetched data, the query retains that data [1]. In this scenario, the query remains in the 'success' status (isSuccess: true), but it also attaches the error to the state (isError: true, error: ErrorObject) [2][1]. This allows you to decide whether to show the old data, the error, or both [1]. Regarding the loading states during this background refetch error: - isFetching: This will be false once the background refetch completes (even if it ends in an error), as the fetch is no longer in-flight [3][2]. - isLoading: This remains false. In v5, isLoading is a derived state defined as (isPending && isFetching) [4][5]. Since you have existing data, the query is in the 'success' status, not the 'pending' status, so isLoading will not be true [2][4]. In summary, when a background refetch fails: - Data is retained [1]. - status is 'success' [2][1]. - isError is true [1]. - isLoading is false [4][5]. - isFetching is false (after the request finishes) [2][6]. If, instead, you are performing an initial fetch (where no data exists) and it fails, the query enters the 'pending' status, and if you trigger a retry, it will return to a 'pending' and 'fetching' state [1][7][8].
Citations:
- 1: Inconsistent refetching state for errored queries TanStack/query#5988
- 2: https://tanstack.com/query/v5/docs/framework/react/guides/queries
- 3: https://tanstack.com/query/v5/docs/framework/react/reference/useQuery
- 4: https://github.com/TanStack/query/blob/feb1efd8/docs/framework/react/guides/migrating-to-v5.md
- 5: https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5
- 6: Differentiate refetching from actual loading TanStack/query#1574
- 7: Reset query error state when calling refetch TanStack/query#8207
- 8: refetchOnMount: false is not respected when the query failed TanStack/query#10018
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
# Read-only truth-table check for the frame's current branch ordering.
states = [
("initial load", True, None, False),
("successful data", False, None, True),
("empty result", False, None, False),
("background refetch failed with cached data", False, "error", True),
]
for name, is_loading, error, has_data in states:
if is_loading:
rendered = "loading"
elif error:
rendered = "error"
elif not has_data:
rendered = "empty"
else:
rendered = "children"
print(f"{name}: current={rendered}; proposed={('loading' if is_loading and not has_data else 'error' if error and not has_data else 'empty' if not has_data else 'children')}")
PY
printf '%s\n' '--- query client defaults ---'
cat -n apps/admin/src/trpc/query-client.tsRepository: superset-sh/superset
Length of output: 1115
Keep cached tile content during background failures.
When a background refetch fails, React Query retains cached data and sets error. Gate the error state on the absence of tile data, and pass the derived data-presence state from each InsightTileFrame caller.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/admin/src/app/`(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx
around lines 53 - 70, Update InsightTileFrame to show the error state only when
no tile data is available, so cached children remain visible during failed
background refetches. Add a derived data-presence prop to InsightTileFrame and
pass it from every caller based on that tile’s cached data, while preserving the
existing loading, empty, and children rendering behavior.
| const listResponse = await fetch( | ||
| "https://api.stripe.com/v1/sigma/scheduled_query_runs?limit=50", | ||
| { headers: authHeader }, | ||
| ); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
New outbound HTTP calls have no request timeout. Every new external call on the admin dashboard path uses bare fetch with no signal, so a stalled upstream holds the tRPC request open without bound. The dashboard fires many of these in parallel on a cold load.
packages/trpc/src/router/analytics/business.ts#L40-L43: passsignal: AbortSignal.timeout(...)to thesigma/scheduled_query_runslist request and to the result-file request on Line 68.packages/trpc/src/lib/posthog-client.ts#L15-L22: passsignal: AbortSignal.timeout(...)to therefresh=blockinginsight request.
📍 Affects 2 files
packages/trpc/src/router/analytics/business.ts#L40-L43(this comment)packages/trpc/src/lib/posthog-client.ts#L15-L22
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/trpc/src/router/analytics/business.ts` around lines 40 - 43,
Configure bounded request timeouts for all listed outbound dashboard calls: in
packages/trpc/src/router/analytics/business.ts at lines 40-43, add an
AbortSignal.timeout(...) signal to the scheduled-query-runs list request and
also to the result-file request around line 68; in
packages/trpc/src/lib/posthog-client.ts at lines 15-22, add the same timeout
signal to the refresh=blocking insight request. Use the existing fetch options
while ensuring every affected call aborts after a defined timeout.
| const [header, ...rows] = csv.trim().split("\n"); | ||
| const columns = (header ?? "").split(","); | ||
| const monthIndex = columns.indexOf("month_end"); | ||
| const mrrIndex = columns.indexOf("total_mrr_in_usd"); | ||
| if (monthIndex === -1 || mrrIndex === -1) { | ||
| return { available: false, reason: "unexpected Sigma CSV columns" }; | ||
| } | ||
|
|
||
| const points = rows | ||
| .map((row) => { | ||
| const cells = row.split(","); | ||
| return { | ||
| monthEnd: cells[monthIndex] ?? "", | ||
| mrrUsd: Number(cells[mrrIndex] ?? Number.NaN), | ||
| }; | ||
| }) | ||
| .filter((p) => p.monthEnd && Number.isFinite(p.mrrUsd)) | ||
| .sort((a, b) => a.monthEnd.localeCompare(b.monthEnd)); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Stripe Sigma scheduled query run result file CSV format quoting
💡 Result:
Stripe Sigma provides query results in CSV format through scheduled query runs and manual dashboard exports [1][2][3]. While Stripe's official documentation confirms that these exports are provided as CSV files [1][2][4][3], it does not explicitly specify the internal technical implementation details of its CSV quoting or escaping rules (e.g., whether it uses RFC 4180 standard, specific enclosure characters, or how it handles delimiters within fields). For practical purposes, Stripe's CSV exports are generated to be compatible with standard spreadsheet applications and data processing tools [2][3]. If you are programmatically consuming these files, you should rely on standard CSV parsing libraries, which typically handle common quoting conventions automatically. You can access these results via: 1. Email notifications: Stripe sends an email with a link to download the results as a CSV file [1][2]. 2. Webhooks: The sigma.scheduled_query_run.created event provides a file object containing a URL to download the CSV [1][5]. You can retrieve this file using your live secret API key [1]. 3. Dashboard: You can manually trigger or download CSV exports directly from the Sigma interface [2][6][3].
Citations:
- 1: https://docs.stripe.com/data/schedule-queries
- 2: https://docs.stripe.com/stripe-data/write-queries?locale=en-GB
- 3: https://docs.stripe.com/data/sigma
- 4: https://stripe.com/docs/api/sigma/scheduled_queries/object?lang=python&s_tact=C43001LW
- 5: https://docs.stripe.com/api/sigma/scheduled_queries
- 6: https://docs.stripe.com/data/write-queries
🏁 Script executed:
#!/bin/bash
set -e
file="packages/trpc/src/router/analytics/business.ts"
sed -n '1,150p' "$file"
printf '\nCSV-related dependencies and parsers:\n'
rg -n '"(csv|papaparse|fast-csv|csv-parse|d3-dsv)|csv|parseCsv|parse.*CSV' --glob 'package.json' --glob 'bun.lock' --glob 'packages/**' .
printf '\nRelevant call sites:\n'
rg -n 'month_end|total_mrr_in_usd|available: false|unexpected Sigma CSV columns' packagesRepository: superset-sh/superset
Length of output: 8329
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' 'Package manifests that declare d3 or CSV parsers:'
rg -n -C 2 '"(d3|d3-dsv|csv-parse|papaparse|fast-csv)"' --glob 'package.json' --glob '!node_modules/**' .
printf '\nBehavior probe for the current parsing logic:\n'
python3 - <<'PY'
import csv
import math
def current(text):
# Equivalent to csv.trim().split("\n"), row.split(","), and the filter.
text = text.strip()
parts = text.split("\n")
header, *rows = parts
columns = header.split(",") if header is not None else [""]
month_index = columns.index("month_end") if "month_end" in columns else -1
mrr_index = columns.index("total_mrr_in_usd") if "total_mrr_in_usd" in columns else -1
points = []
if month_index == -1 or mrr_index == -1:
return ("unexpected Sigma CSV columns", [])
for row in rows:
cells = row.split(",")
month = cells[month_index] if month_index < len(cells) else ""
raw = cells[mrr_index] if mrr_index < len(cells) else "NaN"
try:
mrr = float(raw)
except ValueError:
mrr = float("nan")
if month and math.isfinite(mrr):
points.append((month, mrr))
return ("available", points)
cases = {
"LF simple": "month_end,total_mrr_in_usd\n2025-01-31,100\n",
"CRLF simple": "month_end,total_mrr_in_usd\r\n2025-01-31,100\r\n",
"quoted comma before MRR": 'month_end,description,total_mrr_in_usd\n2025-01-31,"plan, annual",100\n',
"quoted comma in month": '"month_end","total_mrr_in_usd"\n"2025-01-31",100\n',
}
for name, text in cases.items():
print(name, "=>", current(text))
print("\nStandard CSV parsing of the same cases:")
for name, text in cases.items():
rows = list(csv.reader(text.splitlines()))
print(name, "=>", rows)
PYRepository: superset-sh/superset
Length of output: 208
🏁 Script executed:
#!/bin/bash
set -u
python3 - <<'PY'
import csv
import math
def js_number(value):
# Model the relevant Number(...) behavior for these CSV values.
value = value.strip()
if value == "":
return 0.0
try:
return float(value)
except ValueError:
return float("nan")
def current(text):
parts = text.strip().split("\n")
header, *rows = parts
columns = header.split(",")
month_index = columns.index("month_end") if "month_end" in columns else -1
mrr_index = columns.index("total_mrr_in_usd") if "total_mrr_in_usd" in columns else -1
if month_index == -1 or mrr_index == -1:
return ("unexpected Sigma CSV columns", [])
points = []
for row in rows:
cells = row.split(",")
month = cells[month_index] if month_index < len(cells) else ""
mrr = js_number(cells[mrr_index] if mrr_index < len(cells) else "NaN")
if month and math.isfinite(mrr):
points.append((month, mrr))
return ("available", points)
cases = {
"LF simple": "month_end,total_mrr_in_usd\n2025-01-31,100\n",
"CRLF simple": "month_end,total_mrr_in_usd\r\n2025-01-31,100\r\n",
"quoted comma before MRR": 'month_end,description,total_mrr_in_usd\n2025-01-31,"plan, annual",100\n',
"quoted fields": '"month_end","total_mrr_in_usd"\n"2025-01-31","100"\n',
"quoted comma in MRR": 'month_end,total_mrr_in_usd\n2025-01-31,"1,000"\n',
}
for name, text in cases.items():
print(name, "current =>", current(text))
print(name, "CSV reader =>", list(csv.reader(text.splitlines())))
PYRepository: superset-sh/superset
Length of output: 920
🌐 Web query:
site:docs.stripe.com Sigma CSV quoted fields CRLF export format
💡 Result:
Sigma Computing—specifically the Sigma component within the Stripe ecosystem—exports query results in CSV format [1][2]. While official documentation does not explicitly detail the specific RFC compliance (such as quoting behavior or line-ending characters like CRLF) for these generated files, standard CSV exports from such platforms typically follow industry-standard practices, which include wrapping fields containing delimiters or line breaks in quotes and using CRLF (Carriage Return + Line Feed) as the row delimiter [1][2]. Users can trigger these exports via the Stripe Dashboard or programmatically through the Sigma Query Run API [1][3][4].
Citations:
- 1: https://docs.stripe.com/data/sigma.md
- 2: https://docs.stripe.com/data/sigma
- 3: https://docs.stripe.com/data/schedule-queries
- 4: https://docs.stripe.com/api/sigma/saved_queries/create?api-version=2026-02-25.preview
Parse the Sigma result with a quote-aware CSV parser.
The current parser treats CRLF headers as invalid and drops valid rows when quoted fields contain commas. Use a parser that handles quoted fields and CRLF line endings.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/trpc/src/router/analytics/business.ts` around lines 77 - 94, Replace
the manual split logic in the Sigma CSV parsing flow with the project’s
quote-aware CSV parser, ensuring it supports quoted commas and CRLF line
endings. Preserve the existing column validation, point construction, filtering,
and sorting behavior around the parsed header and rows.
| }>(sql` | ||
| WITH subs AS ( | ||
| SELECT created_at, ended_at, date_trunc('month', created_at) AS cohort | ||
| FROM subscriptions | ||
| WHERE status != 'incomplete' AND plan != 'enterprise' | ||
| AND created_at >= date_trunc('month', now()) - make_interval(months => ${input.months}) | ||
| ) | ||
| SELECT | ||
| to_char(cohort, 'YYYY-MM') AS cohort_month, | ||
| k.k AS month_offset, | ||
| count(*)::int AS cohort_size, | ||
| round( | ||
| 100.0 * count(*) FILTER ( | ||
| WHERE ended_at IS NULL OR ended_at >= created_at + make_interval(months => k.k) | ||
| ) / count(*), | ||
| 1 | ||
| )::float AS surviving_pct | ||
| FROM subs | ||
| CROSS JOIN generate_series(0, ${input.months}) AS k(k) | ||
| WHERE created_at + make_interval(months => k.k) <= now() | ||
| GROUP BY cohort_month, k.k | ||
| ORDER BY cohort_month, k.k | ||
| `); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Cohort survival uses a shrinking denominator.
WHERE created_at + make_interval(months => k.k) <= now() filters individual subscription rows, not whole cohorts. For each larger k, subscriptions created later inside the cohort month drop out of the group. So cohort_size differs per month_offset, and surviving_pct divides by that reduced count. Survival reads high because short-lived recent subscriptions leave the denominator.
A cohort heatmap needs one fixed denominator per cohort and a per-cohort cutoff for how many offsets are complete. Compute cohort_size once per cohort, then gate offsets on the cohort month boundary instead of each subscription's own anniversary.
The static analysis SQL-injection findings on this query and the other three are false positives. input.months and input.weeks are zod-validated numbers, and drizzle binds template values as parameters.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/trpc/src/router/analytics/business.ts` around lines 118 - 140,
Update the cohort survival query around the subscriptions CTE and month-offset
aggregation so cohort_size is computed once per cohort and reused as the fixed
denominator for every reported offset. Replace the per-subscription anniversary
filter on created_at with a cohort-level cutoff based on the cohort month
boundary, while retaining only offsets whose cohort period is complete and
preserving the existing output fields and ordering.
Source: Linters/SAST tools
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/trpc/src/router/analytics/business.ts`:
- Around line 85-93: Update the row mapping that builds points so the MRR cell
is trimmed and blank values become Number.NaN before numeric conversion.
Preserve numeric conversion for non-empty values, allowing the existing
Number.isFinite filter to exclude empty MRR cells while retaining valid zero
values.
In `@plans/done/20260812-1845-admin-company-dashboard.md`:
- Around line 116-120: Update the completed plan’s procedure references to match
the shipped business router, replacing business.getRevenueTrend,
business.getNrr, and business.getActivatedToPaid with the exported procedures
actually used by the dashboard. Correct the signup-to-paid input description by
removing user_signed_up and stating that the calculation uses users.created_at,
including the affected sections and repeated references.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a078828d-6cdf-4413-a1ad-97644859ad4c
📒 Files selected for processing (52)
apps/admin/src/app/(dashboard)/components/AppSidebar/AppSidebar.tsxapps/admin/src/app/(dashboard)/components/ArpuTile/ArpuTile.tsxapps/admin/src/app/(dashboard)/components/ArpuTile/index.tsapps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsxapps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/index.tsapps/admin/src/app/(dashboard)/components/DemoCountdown/DemoCountdown.tsxapps/admin/src/app/(dashboard)/components/DemoCountdown/index.tsapps/admin/src/app/(dashboard)/components/HogQLLineTile/HogQLLineTile.tsxapps/admin/src/app/(dashboard)/components/HogQLLineTile/index.tsapps/admin/src/app/(dashboard)/components/InsightTileFrame/InsightTileFrame.tsxapps/admin/src/app/(dashboard)/components/InsightTileFrame/index.tsapps/admin/src/app/(dashboard)/components/LeaderboardTable/LeaderboardTable.tsxapps/admin/src/app/(dashboard)/components/LeaderboardTable/index.tsapps/admin/src/app/(dashboard)/components/LogoRetentionTile/LogoRetentionTile.tsxapps/admin/src/app/(dashboard)/components/LogoRetentionTile/index.tsapps/admin/src/app/(dashboard)/components/MrrTile/MrrTile.tsxapps/admin/src/app/(dashboard)/components/MrrTile/index.tsapps/admin/src/app/(dashboard)/components/PostHogFunnelTile/PostHogFunnelTile.tsxapps/admin/src/app/(dashboard)/components/PostHogFunnelTile/index.tsapps/admin/src/app/(dashboard)/components/RetentionCard/RetentionCard.tsxapps/admin/src/app/(dashboard)/components/RetentionCard/index.tsapps/admin/src/app/(dashboard)/components/RetentionGridTile/RetentionGridTile.tsxapps/admin/src/app/(dashboard)/components/RetentionGridTile/index.tsapps/admin/src/app/(dashboard)/components/RevenueTrendChart/RevenueTrendChart.tsxapps/admin/src/app/(dashboard)/components/RevenueTrendChart/index.tsapps/admin/src/app/(dashboard)/components/SignupToPaidTile/SignupToPaidTile.tsxapps/admin/src/app/(dashboard)/components/SignupToPaidTile/index.tsapps/admin/src/app/(dashboard)/components/SignupsTrendChart/SignupsTrendChart.tsxapps/admin/src/app/(dashboard)/components/SignupsTrendChart/index.tsapps/admin/src/app/(dashboard)/components/TimeRangePicker/TimeRangePicker.tsxapps/admin/src/app/(dashboard)/components/TimeRangePicker/index.tsapps/admin/src/app/(dashboard)/components/TrafficSourcesChart/TrafficSourcesChart.tsxapps/admin/src/app/(dashboard)/components/TrafficSourcesChart/index.tsapps/admin/src/app/(dashboard)/components/TrendSeriesTile/TrendSeriesTile.tsxapps/admin/src/app/(dashboard)/components/TrendSeriesTile/index.tsapps/admin/src/app/(dashboard)/components/WAUTrendChart/WAUTrendChart.tsxapps/admin/src/app/(dashboard)/components/WAUTrendChart/index.tsapps/admin/src/app/(dashboard)/components/WeekPicker/WeekPicker.tsxapps/admin/src/app/(dashboard)/components/WeekPicker/index.tsapps/admin/src/app/(dashboard)/page.tsxapps/admin/src/app/(dashboard)/users/components/UsersTable/UsersTable.tsxapps/admin/src/app/(dashboard)/users/components/UsersTable/index.tsapps/admin/src/app/(dashboard)/users/page.tsxpackages/trpc/src/env.tspackages/trpc/src/index.tspackages/trpc/src/lib/posthog-client.tspackages/trpc/src/root.tspackages/trpc/src/router/admin/admin.tspackages/trpc/src/router/analytics/analytics.tspackages/trpc/src/router/analytics/business.tspackages/trpc/src/router/analytics/insight-registry.tsplans/done/20260812-1845-admin-company-dashboard.md
💤 Files with no reviewable changes (21)
- apps/admin/src/app/(dashboard)/users/page.tsx
- apps/admin/src/app/(dashboard)/components/WAUTrendChart/index.ts
- apps/admin/src/app/(dashboard)/components/SignupsTrendChart/SignupsTrendChart.tsx
- apps/admin/src/app/(dashboard)/components/WeekPicker/index.ts
- apps/admin/src/app/(dashboard)/components/TrafficSourcesChart/index.ts
- apps/admin/src/app/(dashboard)/components/TrafficSourcesChart/TrafficSourcesChart.tsx
- apps/admin/src/app/(dashboard)/users/components/UsersTable/UsersTable.tsx
- apps/admin/src/app/(dashboard)/components/RetentionCard/index.ts
- apps/admin/src/app/(dashboard)/components/LeaderboardTable/index.ts
- apps/admin/src/app/(dashboard)/components/DemoCountdown/index.ts
- apps/admin/src/app/(dashboard)/components/RevenueTrendChart/RevenueTrendChart.tsx
- apps/admin/src/app/(dashboard)/components/DemoCountdown/DemoCountdown.tsx
- apps/admin/src/app/(dashboard)/components/WeekPicker/WeekPicker.tsx
- apps/admin/src/app/(dashboard)/components/RetentionCard/RetentionCard.tsx
- apps/admin/src/app/(dashboard)/components/LeaderboardTable/LeaderboardTable.tsx
- apps/admin/src/app/(dashboard)/users/components/UsersTable/index.ts
- apps/admin/src/app/(dashboard)/components/RevenueTrendChart/index.ts
- apps/admin/src/app/(dashboard)/components/TimeRangePicker/index.ts
- apps/admin/src/app/(dashboard)/components/TimeRangePicker/TimeRangePicker.tsx
- apps/admin/src/app/(dashboard)/components/SignupsTrendChart/index.ts
- apps/admin/src/app/(dashboard)/components/WAUTrendChart/WAUTrendChart.tsx
🚧 Files skipped from review as they are similar to previous changes (28)
- apps/admin/src/app/(dashboard)/components/LogoRetentionTile/index.ts
- apps/admin/src/app/(dashboard)/components/HogQLLineTile/index.ts
- apps/admin/src/app/(dashboard)/components/AppSidebar/AppSidebar.tsx
- apps/admin/src/app/(dashboard)/components/ArpuTile/index.ts
- packages/trpc/src/root.ts
- apps/admin/src/app/(dashboard)/components/MrrTile/index.ts
- apps/admin/src/app/(dashboard)/components/InsightTileFrame/InsightTileFrame.tsx
- apps/admin/src/app/(dashboard)/components/HogQLLineTile/HogQLLineTile.tsx
- apps/admin/src/app/(dashboard)/components/RetentionGridTile/index.ts
- packages/trpc/src/index.ts
- packages/trpc/src/env.ts
- packages/trpc/src/router/analytics/insight-registry.ts
- apps/admin/src/app/(dashboard)/components/RetentionGridTile/RetentionGridTile.tsx
- apps/admin/src/app/(dashboard)/components/TrendSeriesTile/index.ts
- apps/admin/src/app/(dashboard)/components/PostHogFunnelTile/PostHogFunnelTile.tsx
- apps/admin/src/app/(dashboard)/components/SignupToPaidTile/index.ts
- apps/admin/src/app/(dashboard)/page.tsx
- apps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/ChurnHeatmapTile.tsx
- apps/admin/src/app/(dashboard)/components/ArpuTile/ArpuTile.tsx
- apps/admin/src/app/(dashboard)/components/PostHogFunnelTile/index.ts
- packages/trpc/src/router/admin/admin.ts
- packages/trpc/src/lib/posthog-client.ts
- apps/admin/src/app/(dashboard)/components/LogoRetentionTile/LogoRetentionTile.tsx
- packages/trpc/src/router/analytics/analytics.ts
- apps/admin/src/app/(dashboard)/components/InsightTileFrame/index.ts
- apps/admin/src/app/(dashboard)/components/TrendSeriesTile/TrendSeriesTile.tsx
- apps/admin/src/app/(dashboard)/components/MrrTile/MrrTile.tsx
- apps/admin/src/app/(dashboard)/components/ChurnHeatmapTile/index.ts
| const points = rows | ||
| .map((row) => { | ||
| const cells = row.split(","); | ||
| return { | ||
| monthEnd: cells[monthIndex] ?? "", | ||
| mrrUsd: Number(cells[mrrIndex] ?? Number.NaN), | ||
| }; | ||
| }) | ||
| .filter((p) => p.monthEnd && Number.isFinite(p.mrrUsd)) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Reject empty MRR cells before numeric conversion.
Number("") returns 0. A blank total_mrr_in_usd value therefore passes Number.isFinite and creates a false zero-MRR point. Trim the cell and treat an empty value as Number.NaN before filtering.
Proposed fix
.map((row) => {
const cells = row.split(",");
+ const rawMrr = cells[mrrIndex]?.trim();
return {
monthEnd: cells[monthIndex] ?? "",
- mrrUsd: Number(cells[mrrIndex] ?? Number.NaN),
+ mrrUsd: rawMrr ? Number(rawMrr) : Number.NaN,
};
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const points = rows | |
| .map((row) => { | |
| const cells = row.split(","); | |
| return { | |
| monthEnd: cells[monthIndex] ?? "", | |
| mrrUsd: Number(cells[mrrIndex] ?? Number.NaN), | |
| }; | |
| }) | |
| .filter((p) => p.monthEnd && Number.isFinite(p.mrrUsd)) | |
| const points = rows | |
| .map((row) => { | |
| const cells = row.split(","); | |
| const rawMrr = cells[mrrIndex]?.trim(); | |
| return { | |
| monthEnd: cells[monthIndex] ?? "", | |
| mrrUsd: rawMrr ? Number(rawMrr) : Number.NaN, | |
| }; | |
| }) | |
| .filter((p) => p.monthEnd && Number.isFinite(p.mrrUsd)) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/trpc/src/router/analytics/business.ts` around lines 85 - 93, Update
the row mapping that builds points so the MRR cell is trimmed and blank values
become Number.NaN before numeric conversion. Preserve numeric conversion for
non-empty values, allowing the existing Number.isFinite filter to exclude empty
MRR cells while retaining valid zero values.
| Re-implemented live in admin against Neon/Stripe (replacing stale-warehouse tiles and placeholders): | ||
|
|
||
| - `zjqYXl1N` "Recurring revenue — monthly recognized" and `uJv3GdAE` "NRR — monthly" → live Stripe-backed procedures (the PostHog versions read the stale Stripe mirror). | ||
| - `DhKKxIJx` "Paid churn — cohort survival heatmap" → live Neon `subscriptions` query. | ||
| - The three placeholders `veHqMB5A` (activated→paid), `wQpHYjlf` (ARPU), `TO8d4iH9` (enterprise ARR) → real implementations, which is the whole reason they were blocked in PostHog. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Align the completed plan with the shipped business router.
The plan names business.getRevenueTrend, business.getNrr, and business.getActivatedToPaid, but the supplied router does not export them. It also describes user_signed_up as a signup-to-paid input after D-4 records that the event was removed. Replace these sections with the shipped procedures and their actual inputs. State that signup-to-paid uses users.created_at.
Also applies to: 134-134, 157-159
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plans/done/20260812-1845-admin-company-dashboard.md` around lines 116 - 120,
Update the completed plan’s procedure references to match the shipped business
router, replacing business.getRevenueTrend, business.getNrr, and
business.getActivatedToPaid with the exported procedures actually used by the
dashboard. Correct the signup-to-paid input description by removing
user_signed_up and stating that the calculation uses users.created_at, including
the affected sections and repeated references.
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
…s in business metrics Found by dry-running against a prod-data Neon branch: users/members live in the auth schema in production (raw SQL bypasses Drizzle's pgSchema mapping), the logo-retention months series stopped one month short so the newest base month read 0% retained, and signup→paid included a partial trailing week. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Replaces the dashboard-managed scheduled Sigma query: getMrr now POSTs Stripe's own MRR template to /v2/data/reporting/query_runs, polls to completion, and parses the CSV — 12h in-process cache, concurrent callers share one run. Verified live: matches the dashboard MRR chart (July 2026: $14,510). Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
… unbilled deals Replaces the not-yet-tracked placeholder: ARR is the annualized amounts of active Stripe subscriptions for plan=enterprise orgs (Neon names which orgs are enterprise; Stripe supplies the money). Deals without a priced Stripe subscription are counted as unbilled logos on the tile instead of silently omitted — the bookkeeping contract is that every enterprise contract exists as a priced Stripe subscription with wires marked paid out-of-band on the subscription invoice. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Removes the leftover stacking wrapper from the ARPA removal; the card now takes the row's full height via h-full (grid items stretch). Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…x axes DAU and WAU are now separate PostHog insights (7mbktvP7 / 82RAL00l) and separate admin tiles. Shared chartAxis util applies the axis rule everywhere: ranges beyond ~2 months tick once per month labeled with the month name, shorter ranges label dates as "Aug 8"; non-date axes (percentiles) pass through unchanged. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Replaces the horizontal Recharts bars with PostHog's funnel layout: one column per step, solid fill as % of step one over a hatched drop-off region, with converted/dropped persons, step-over-step %, and median conversion time under each column. The OR-group step gets its real name instead of the '$pageview, $pageview, …' mush. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Funnel columns get two hover targets — solid region shows converted / conversion-from-previous / conversion-so-far / median+average time, hatched region shows dropped / drop-off-from-previous / drop-off-from- start — with full-contrast tooltip values. Retention grid redesigned as rounded chips with a size-weighted Mean row, Week N headers, decimal percentages, dashed outline for the in-progress diagonal cell, and no cells for future weeks (previously misleading 0%s). Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…tip spacing WAU insight switched from rolling-7d to calendar-week intervals (one entry per week, PostHog pattern from the Superset Dashboard's WAU chart). Weekly tiles (WAU, p50/p90, download CTR) draw the in-progress week as a dashed final segment. Shared chart tooltip gains a gap between label and value so long labels don't collide with numbers. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
bg-background container with border, muted-foreground labels, and foreground mono values — identical contrast to the shared chart tooltips instead of the UI tooltip primitive's primary tokens. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Tiles are organized by who cares, and can appear on several tabs: Company shows DAU/WAU/MRR/Enterprise ARR; Product shows the activation funnel, DAU/WAU, activated rate, active orgs, workspace depth, and cohort retention; Growth shows the funnel, site visitors, download CTR, signup→paid, activated rate, MRR, logo retention, and churn cohorts. Enterprise ARR extracted into its own tile component. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…mpany tab getCashFlow reads Mercury accounts + Treasury (where the raise is parked) for total cash, and aggregates monthly transactions across all accounts with treasury sweeps excluded. Runway = cash / avg gross ops outflows over the last 3 complete months — net flow includes fundraise tranche wires, so it decorates the cash card instead of driving runway or the chart. Company tab gains a Cash & runway card and a monthly gross-burn chart. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…y, burn by vendor Headline stats (cash / gross burn / runway) lead the tab, followed by the total-cash balance over time (tranche wires as steps), monthly gross burn, and average monthly outflow per counterparty over the last 3 complete months — the actionable view of where money goes. Cash series is reconstructed by walking transactions back from today's balances. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
… fix Net burn nets Stripe payouts against outflows (runway now 211 mo at $53k/mo net vs $69.5k gross); the burn chart plots net burn beside Stripe revenue. Enterprise ARR becomes a per-account list with org name, logo, and contracted ARR from Stripe — unbilled deals stay visible. Retention Mean row now includes weeks only reached by in-progress cohorts, rendering dashed like the cohort cells instead of blank. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…usive Enterprise subscriptions bill through Stripe, so their ARR is already counted in the Sigma MRR line — the tile now says so to stop anyone adding the two. Cash card notes it includes raised capital. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
The only sub-zero value is the partial current month, which was pulling the axis to -$50k and wasting half the chart. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
…rn months
Analytics dates are calendar strings, but new Date('2026-02-01') parses
as UTC midnight and formatted in local time — every date axis was
labeled one day early, rendering February as January. All formatting now
pins to UTC in the shared chartAxis util.
Retention and churn-survival grids now share one CohortGrid component so
they can't drift apart: chips scaled by percentage, dashed while a
period is in progress, blank for periods not yet reached. Churn gains
the retention grid's styling (it was still the old smushed pills).
The burn chart drops months with no outflows — pre-operating noise that
produced negative burn and dragged the axis to -$50k.
Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Drops the Cash and Net burn stat cards — both duplicated their charts. Cash trajectory goes full width, with the net burn chart and a runway card (carrying the cash ÷ burn inputs in its subtitle) side by side underneath. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Stat cards stretched to match chart-height siblings left their value pinned to the top-left of a mostly empty card. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
Cash, burn, and runway on the admin dashboard read Mercury. Production only — preview deploys leave it unset (it is a live banking credential and preview URLs are more exposed), where the tiles degrade to an explicit "not configured" state. Claude-Session: https://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ
What
The admin app becomes the company metrics dashboard: Company / Product / Growth tabs over a mirror of the PostHog Success Metrics dashboard, plus live business metrics from Stripe, Neon, and Mercury. Stale admin surfaces are deleted.
Company — cash trajectory (full width), net burn, runway, MRR, Enterprise ARR by account, WAU, DAU, burn by vendor
Product — activation funnel, DAU/WAU, activated rate, active orgs, workspace depth, cohort retention
Growth — activation funnel, new site visitors, download CTR, signup→paid, activated rate, MRR, logo retention, churn survival
Where each number comes from
Each system answers only what it is authoritative for:
short_idthroughanalytics.getInsightResults(allowlisted registry inpackages/trpc).refresh=blockingwith no app-side cache: PostHog's own result cache and refresh throttle are the caching layer.Methodology notes worth reviewing
plan='enterprise'throughout.Purged
Users page (unbounded
listUsersfindMany — the row-count freeze), theauth_completedretention card and procedures (that event is no longer emitted), WAU/signups/revenue/traffic charts, leaderboard, demo countdown, range pickers, and the now-unused KV cache layer inposthog-client.Notable fixes found along the way
new Date("2026-02-01")parses as UTC midnight and formatted locally renders as January. All formatting now pins to UTC.CohortGridcomponent so they can't drift.posthog-nodeinto client bundles.Deploy
MERCURY_API_TOKENis set as a production environment secret and passed to the api and admin deploys. Preview deploys intentionally leave it unset (live banking credential); tiles degrade to an explicit "not configured" state.Plan with the full decision log:
plans/done/20260812-1845-admin-company-dashboard.mdhttps://claude.ai/code/session_01BZReT2T6zEDJ8rFUry31nJ